using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;

/// <summary>
/// A generic IComparer, primarily for sorting mesh segments by z value, to avoid overlap and Z issues.
/// </summary>
/// <typeparam name="T"></typeparam>
public class Ferr2DT_Comparer<T> : IComparer<T> {
    private readonly Func<T, T, int> func;
    public Ferr2DT_Comparer(Func<T, T, int> comparerFunc) {
        this.func = comparerFunc;
    }

    public int Compare(T x, T y) {
        return this.func(x, y);
    }
}

/// <summary>
/// Describes how the terrain path should be filled.
/// </summary>
public enum Ferr2DT_FillMode
{
    /// <summary>
    /// The interior of the path will be filled, and edges will be treated like a polygon.
    /// </summary>
    Closed,
    /// <summary>
    /// Drops some extra vertices down, and fill the interior. Edges only around the path itself.
    /// </summary>
    Skirt,
    /// <summary>
    /// Doesn't fill the interior at all. Just edges.
    /// </summary>
    None,
    /// <summary>
    /// Fills the outside of the path rather than the interior, also inverts the edges, upside-down.
    /// </summary>
    InvertedClosed
}

[AddComponentMenu("Ferr2DT/Path Terrain"), RequireComponent (typeof (Ferr2D_Path)), RequireComponent (typeof (MeshFilter)), RequireComponent (typeof (MeshRenderer))]
public class Ferr2DT_PathTerrain : MonoBehaviour, Ferr2D_IPath {
	
	#region Public fields
	public Ferr2DT_FillMode fill    = Ferr2DT_FillMode.Closed;
    /// <summary>
    /// If fill is set to Skirt, this value represents the Y value of where the skirt will end.
    /// </summary>
	public float    fillY           = 0;
    /// <summary>
    /// In order to combat Z-Fighting, this allows you to set a Z-Offset on the fill.
    /// </summary>
	public float    fillZ           = 0.2f;
    /// <summary>
    /// This will separate edges at corners, for applying different material parts to different slopes,
    /// as well as creating sharp corners on smoothed paths.
    /// </summary>
	public bool     splitCorners    = true;
    /// <summary>
    /// Makes the path curvy. It's not a perfect algorithm just yet, but it does make things curvier.
    /// </summary>
    public bool     smoothPath      = false;
    /// <summary>
    /// On smoothed surfaces, the distance between each split on the curve (Unity units)
    /// </summary>
    public float    splitDist       = 4;
    /// <summary>
    /// Roughly how many pixels we try to fit into one unit of Unity space
    /// </summary>
	public float    pixelsPerUnit   = 32;
    /// <summary>
    /// The color for every vertex! If you use the right shader (like the Ferr2D shaders) this will influence
    /// the color of the terrain. This is faster, because you don't need additional materials for new colors!
    /// </summary>
    public Color    vertexColor     = Color.white;

    /// <summary>
    /// Should we generate a collider on Start?
    /// </summary>
    public bool     createCollider  = true;
    /// <summary>
    /// How wide should the collider be on the Z axis? (Unity units)
    /// </summary>
    public float    depth           = 4.0f;
    /// <summary>
    /// For offseting the collider, so it can line up with stuff better visually. On fill = None terrain,
    /// this behaves significantly different than regular closed terrain.
    /// </summary>
    public float[]  surfaceOffset   = new float[] {0,0,0,0};
    
    /// <summary>
    /// This property will call SetMaterial when set.
    /// </summary>
    public Ferr2DT_TerrainMaterial TerrainMaterial { get { return terrainMaterial; } set { SetMaterial(value); } }
	#endregion
	
	#region Private fields
    [SerializeField()]
    Ferr2DT_TerrainMaterial terrainMaterial;
	Ferr2D_Path            path;
	Ferr2DT_DynamicMesh     dMesh;
	Vector2                 unitsPerUV = Vector2.one;
	#endregion

    #region MonoBehaviour Methods
    void Start()
    {
        if (createCollider)
        {
            RecreateCollider();
        }
        for (int i = 0; i < Camera.allCameras.Length; i++)
        {
            Camera.allCameras[i].transparencySortMode = TransparencySortMode.Orthographic;
        }
    }
    #endregion

    #region Creation methods
    /// <summary>
    /// The Ferr2DT_IPath method, gets called automatically whenever the Ferr2DT path gets updated in the 
    /// editor. This will completely recreate the the visual mesh (only) for the terrain. If you want
    /// To recreate the collider as well, that's a separate call to RecreateCollider.
    /// </summary>
    public  void RecreatePath    () {
        //System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        //sw.Start();

		if (path			== null) path			 = GetComponent<Ferr2D_Path> ();
		if (dMesh			== null) dMesh			 = new Ferr2DT_DynamicMesh    ();
        if (terrainMaterial == null)
        {
            Debug.LogWarning("Cannot create terrain without a Terrain Material!");
            return;
        }

        // double check the materials!
        if (renderer.sharedMaterials[0] != terrainMaterial.edgeMaterial || renderer.sharedMaterials[1] != terrainMaterial.fillMaterial)
            ForceMaterial(terrainMaterial, true);
		
		dMesh.Clear ();
        dMesh.color = vertexColor;
		
		if (path.Count < 2) {
			GetComponent<MeshFilter>().sharedMesh = null;
			return;
		}
		
		// make sure we can keep a consistent scale for the texture
		if (renderer.sharedMaterial && renderer.sharedMaterial.HasProperty("_MainTex")) {
			Texture tex = renderer.sharedMaterial.GetTexture("_MainTex");
			if (tex != null) {
				unitsPerUV.x = renderer.sharedMaterial.GetTexture("_MainTex").width  / pixelsPerUnit;
				unitsPerUV.y = renderer.sharedMaterial.GetTexture("_MainTex").height / pixelsPerUnit;
			}
		}
		
		// split the path into segments based on the split angle
		List<List<Vector2>> segments    = new List<List<Vector2>>();
        //segments = GetSegments(path.pathVerts);
        segments = GetSegments(path.GetVerts(smoothPath,splitDist,splitCorners));
        segments.Sort(
            new Ferr2DT_Comparer<List<Vector2>>(
                (x, y) => GetDescription(y).zOffset.CompareTo(GetDescription(x).zOffset)
            ));
		
		// process the segments into meshes
		for (int i = 0; i < segments.Count; i++) {
			AddSegment (segments[i], segments.Count <= 1 && path.closed);
		}
		int[] submesh1 = dMesh.GetCurrentTriangleList();
		
		// add a fill if the user desires
        if (fill == Ferr2DT_FillMode.Skirt && terrainMaterial.fillMaterial != null)
        {
			AddFill(true);
        }
        else if ((fill == Ferr2DT_FillMode.Closed || fill == Ferr2DT_FillMode.InvertedClosed) && terrainMaterial.fillMaterial != null)
        {
            AddFill(false);
        }
        else if (fill == Ferr2DT_FillMode.None) { }
		int[] submesh2 = dMesh.GetCurrentTriangleList(submesh1.Length);
		
		// compile the mesh!
		Mesh   m    = GetComponent<MeshFilter>().sharedMesh;
		string name = "Ferr2DT_PathMesh_" + gameObject.name + gameObject.GetInstanceID();
		if (m == null || m.name != name) {
			GetComponent<MeshFilter>().sharedMesh = m = new Mesh();
			m.name = name;
		}
		dMesh.Build(ref m);
		
		// set up submeshes and submaterials
		m.subMeshCount=2;
		if (renderer.sharedMaterials.Length < 2) {
			Material[] old = renderer.sharedMaterials;
			renderer.sharedMaterials = new Material[2];
			if (old.Length > 0) renderer.sharedMaterials[0] = old[0];
		}
		m.SetTriangles(submesh1,0);
		m.SetTriangles(submesh2,1);
        //sw.Stop();
        //Debug.Log("Creating mesh took: " + sw.Elapsed.TotalMilliseconds + "ms");
	}
    /// <summary>
    /// Creates a mesh and adds it to the MeshCollider object. This is automatically calld on Start,
    /// if createCollider is set to true. This will automatically add a MeshCollider if none is 
    /// attached already.
    /// </summary>
    public  void RecreateCollider()
    {
        Ferr2DT_DynamicMesh dMesh = new Ferr2DT_DynamicMesh();
        Ferr2D_Path        path  = GetComponent<Ferr2D_Path>();
        List<Vector2>       verts = path.GetVerts(smoothPath, splitDist, splitCorners);
        if (path.closed && smoothPath == false) verts.Add(verts[0]);
        List<Vector2>       raw   = new List<Vector2>(verts);

        // consider the offset values for the collider. Also, if it's not filled, make sure we add in 
        // extra path verts for the underside of the path
        int count = raw.Count;
        for (int i = count - 1; i >= 0; i--)
        {
            Vector2 norm = Ferr2D_Path.GetNormal(raw, i, path.closed);
            if (fill == Ferr2DT_FillMode.None)
            {
                verts.Add(raw[i] +  new Vector2(norm.x *  surfaceOffset[(int)Ferr2DT_TerrainDirection.Top   ], norm.y *  surfaceOffset[(int)Ferr2DT_TerrainDirection.Top   ]));
                verts        [i] += new Vector2(norm.x * -surfaceOffset[(int)Ferr2DT_TerrainDirection.Bottom], norm.y * -surfaceOffset[(int)Ferr2DT_TerrainDirection.Bottom]);
            }
            else
            {
                Vector2 offset = GetOffset(raw, i, path.closed);
                verts[i] += new Vector2(norm.x * -offset.x, norm.y * -offset.y);
            }
        }

        if (fill == Ferr2DT_FillMode.Skirt)
        {
            Vector2 start = verts[0];
            Vector2 end   = verts[verts.Count - 1];
            verts.Add(new Vector2(end.x, fillY));
            verts.Add(new Vector2(Mathf.Lerp(end.x, start.x, 0.33f), fillY));
            verts.Add(new Vector2(Mathf.Lerp(end.x, start.x, 0.66f), fillY));
            verts.Add(new Vector2(start.x, fillY));
        }
        else if (fill == Ferr2DT_FillMode.None)
        {
        }

        // triangulate, and create the solid mesh for it
        List<int> indices = Ferr2DT_Triangulator.GetIndices(ref verts, true, fill == Ferr2DT_FillMode.InvertedClosed);
        for (int i = 0; i < verts.Count; i++)
        {
            if (path.closed && i == verts.Count-1) dMesh.AddVertex(verts[0]);
            else dMesh.AddVertex(verts[i]);
        }
        for (int i = 0; i < indices.Count; i += 3)
        {
            dMesh.AddFace(indices[i], indices[i + 1], indices[i + 2]);
        }
        dMesh.ExtrudeZ(depth, fill == Ferr2DT_FillMode.InvertedClosed);

        // make sure there's a MeshCollider component on this object
        if (GetComponent<MeshCollider>() == null)
        {
            gameObject.AddComponent<MeshCollider>();
        }

        // compile the mesh!
        Mesh m = GetComponent<MeshCollider>().sharedMesh;
        string name = "Ferr2DT_PathCollider_" + gameObject.name + gameObject.GetInstanceID();
        if (m == null || m.name != name)
        {
            GetComponent<MeshCollider>().sharedMesh = m = new Mesh();
            m.name = name;
        }
        GetComponent<MeshCollider>().sharedMesh = null;
        dMesh.Build(ref m);
        GetComponent<MeshCollider>().sharedMesh = m;
    }
	#endregion
	
	#region Mesh manipulation methods
	private void AddSegment(List<Vector2> aSegment, bool aClosed) {
		Ferr2DT_SegmentDescription	desc 		= GetDescription(aSegment);
        int                         bodyID      = UnityEngine.Random.Range(0, desc.body.Length);
		Rect                        body        = terrainMaterial.ToUV( desc.body[bodyID] );
		float						bodyWidth	= body.width * unitsPerUV.x;

        int tSeed = UnityEngine.Random.seed;

        Vector2 capLeftSlideDir  = (aSegment[1] - aSegment[0]);
        Vector2 capRightSlideDir = (aSegment[aSegment.Count-2] - aSegment[aSegment.Count-1]);
        capLeftSlideDir .Normalize();
        capRightSlideDir.Normalize();
        aSegment[0               ] -= capLeftSlideDir  * desc.capOffset;
        aSegment[aSegment.Count-1] -= capRightSlideDir * desc.capOffset;

		for (int i = 0; i < aSegment.Count-1; i++) {
			Vector2 norm1   = Vector2.zero;
			Vector2 norm2   = Vector2.zero;
			float   length  = Vector2.Distance(aSegment[i+1],aSegment[i]);
			int     repeats = Mathf.Max(1,Mathf.FloorToInt(length / bodyWidth));
			
			norm1 = Ferr2D_Path.GetNormal(aSegment, i,   aClosed);
			norm2 = Ferr2D_Path.GetNormal(aSegment, i+1, aClosed);
			
			for (int t = 1; t < repeats+1; t++) {
                UnityEngine.Random.seed = (int)(transform.position.x * 100000 + transform.position.y * 10000 + i * 100 + t);
                body                    = terrainMaterial.ToUV(desc.body[UnityEngine.Random.Range(0, desc.body.Length)]);
                Vector2 pos1, pos2, n1, n2;

                pos1 = Vector2.Lerp(aSegment[i], aSegment[i + 1], (float)(t - 1) / repeats);
                pos2 = Vector2.Lerp(aSegment[i], aSegment[i + 1], (float)t / repeats);
                n1   = Vector2.Lerp(norm1, norm2, (float)(t - 1) / repeats);
                n2   = Vector2.Lerp(norm1, norm2, (float)t / repeats);

                float d    = (body.height / 2) * unitsPerUV.y;
                float yOff = fill == Ferr2DT_FillMode.InvertedClosed ? -desc.yOffset : desc.yOffset;
                int   v1 = dMesh.AddVertex(pos1.x + n1.x * (d + yOff), pos1.y + n1.y * (d + yOff), desc.zOffset, body.x,    fill == Ferr2DT_FillMode.InvertedClosed ? body.yMax : body.y);
                int   v2 = dMesh.AddVertex(pos1.x - n1.x * (d - yOff), pos1.y - n1.y * (d - yOff), desc.zOffset, body.x,    fill == Ferr2DT_FillMode.InvertedClosed ? body.y    : body.yMax);
                int   v3 = dMesh.AddVertex(pos2.x + n2.x * (d + yOff), pos2.y + n2.y * (d + yOff), desc.zOffset, body.xMax, fill == Ferr2DT_FillMode.InvertedClosed ? body.yMax : body.y);
                int   v4 = dMesh.AddVertex(pos2.x - n2.x * (d - yOff), pos2.y - n2.y * (d - yOff), desc.zOffset, body.xMax, fill == Ferr2DT_FillMode.InvertedClosed ? body.y    : body.yMax);
				dMesh.AddFace(v1, v3, v4, v2);
			}
		}
        if (!aClosed)
        {
            AddCap(aSegment, desc, -1);
            AddCap(aSegment, desc, 1);
        }
        UnityEngine.Random.seed = tSeed;
	}
	private void AddCap    (List<Vector2> aSegment, Ferr2DT_SegmentDescription aDesc, float aDir) {
		int     index = 0;
		Vector2 dir   = Vector2.zero;
		if (aDir < 0) {
			index = 0;
			dir   = aSegment[0] - aSegment[1];
		} else {
			index = aSegment.Count-1;
			dir   = aSegment[aSegment.Count-1] - aSegment[aSegment.Count-2];
		}
		dir.Normalize();
		Vector2 norm = Ferr2D_Path.GetNormal(aSegment, index, false);
		Vector2 pos  = aSegment[index];
        Rect    lCap = fill == Ferr2DT_FillMode.InvertedClosed ? terrainMaterial.ToUV(aDesc.rightCap) : terrainMaterial.ToUV(aDesc.leftCap);
        Rect    rCap = fill == Ferr2DT_FillMode.InvertedClosed ? terrainMaterial.ToUV(aDesc.leftCap ) : terrainMaterial.ToUV(aDesc.rightCap);
        float   yOff = fill == Ferr2DT_FillMode.InvertedClosed ? -aDesc.yOffset : aDesc.yOffset;

		if (aDir < 0) {
			float width =  lCap.width     * unitsPerUV.x;
			float scale = (lCap.height/2) * unitsPerUV.y;

            int v1 = dMesh.AddVertex(pos + dir * width + norm * (scale + yOff), aDesc.zOffset, new Vector2(fill == Ferr2DT_FillMode.InvertedClosed? lCap.xMax : lCap.x, fill == Ferr2DT_FillMode.InvertedClosed ? lCap.yMax : lCap.y));
            int v2 = dMesh.AddVertex(pos + norm * (scale + yOff), aDesc.zOffset, new Vector2(fill == Ferr2DT_FillMode.InvertedClosed ? lCap.x : lCap.xMax, fill == Ferr2DT_FillMode.InvertedClosed ? lCap.yMax : lCap.y));

            int v3 = dMesh.AddVertex(pos - norm * (scale - yOff), aDesc.zOffset, new Vector2(fill == Ferr2DT_FillMode.InvertedClosed ? lCap.x : lCap.xMax, fill == Ferr2DT_FillMode.InvertedClosed ? lCap.y : lCap.yMax));
            int v4 = dMesh.AddVertex(pos + dir * width - norm * (scale - yOff), aDesc.zOffset, new Vector2(fill == Ferr2DT_FillMode.InvertedClosed ? lCap.xMax : lCap.x, fill == Ferr2DT_FillMode.InvertedClosed ? lCap.y : lCap.yMax));
			dMesh.AddFace(v1, v2, v3, v4);
		} else {
			float width =  rCap.width     * unitsPerUV.x;
			float scale = (rCap.height/2) * unitsPerUV.y;

            int v1 = dMesh.AddVertex(pos + dir * width + norm * (scale + yOff), aDesc.zOffset, new Vector2(fill == Ferr2DT_FillMode.InvertedClosed ? rCap.x : rCap.xMax, fill == Ferr2DT_FillMode.InvertedClosed ? rCap.yMax : rCap.y));
            int v2 = dMesh.AddVertex(pos + norm * (scale + yOff),               aDesc.zOffset, new Vector2(fill == Ferr2DT_FillMode.InvertedClosed ? rCap.xMax : rCap.x, fill == Ferr2DT_FillMode.InvertedClosed ? rCap.yMax : rCap.y));

            int v3 = dMesh.AddVertex(pos - norm * (scale - yOff),               aDesc.zOffset, new Vector2(fill == Ferr2DT_FillMode.InvertedClosed ? rCap.xMax : rCap.x, fill == Ferr2DT_FillMode.InvertedClosed ? rCap.y : rCap.yMax));
            int v4 = dMesh.AddVertex(pos + dir * width - norm * (scale - yOff), aDesc.zOffset, new Vector2(fill == Ferr2DT_FillMode.InvertedClosed ? rCap.x : rCap.xMax, fill == Ferr2DT_FillMode.InvertedClosed ? rCap.y : rCap.yMax));
			dMesh.AddFace(v4, v3, v2, v1);
		}
	}
	private void AddFill   (bool aSkirt) {
        List<Vector2> fillVerts = new List<Vector2>(path.GetVerts(smoothPath,splitDist,splitCorners));
        Vector2       scale     = Vector2.one;

        // scale is different for the fill texture
        if (terrainMaterial.fillMaterial != null && terrainMaterial.fillMaterial.mainTexture != null)
        {
            scale = new Vector2(
            terrainMaterial.fillMaterial.mainTexture.width / pixelsPerUnit,
            terrainMaterial.fillMaterial.mainTexture.height / pixelsPerUnit);
        }

        if (aSkirt)
        {
            Vector2 start = fillVerts[0];
            Vector2 end   = fillVerts[fillVerts.Count - 1];

            fillVerts.Add(new Vector2(end.x, fillY));
            fillVerts.Add(new Vector2(Mathf.Lerp(end.x, start.x, 0.33f), fillY));
            fillVerts.Add(new Vector2(Mathf.Lerp(end.x, start.x, 0.66f), fillY));
            fillVerts.Add(new Vector2(start.x, fillY));
        }

        int       offset  = dMesh.VertCount;
        List<int> indices = Ferr2DT_Triangulator.GetIndices(ref fillVerts, true, fill == Ferr2DT_FillMode.InvertedClosed);
        for (int i = 0; i < fillVerts.Count; i++)
        {
            dMesh.AddVertex(fillVerts[i].x, fillVerts[i].y, fillZ, fillVerts[i].x / scale.x, fillVerts[i].y / scale.y);
        }
        for (int i = 0; i < indices.Count; i+=3)
        {
            dMesh.AddFace(indices[i  ] + offset,
                          indices[i+1] + offset,
                          indices[i+2] + offset);
        }
	}
	#endregion
	
	#region Supporting methods
    /// <summary>
    /// Sets the material of the mesh. Calls ForceMaterial with an aForceUpdate of false.
    /// </summary>
    /// <param name="aMaterial">The terrain material! Usually from a terrain material prefab.</param>
    public  void                        SetMaterial     (Ferr2DT_TerrainMaterial aMaterial) 
    { 
        ForceMaterial(aMaterial, false); 
    }
    /// <summary>
    /// This will allow you to set the terrain material regardless of whether it's marked as the current material already or not. Also calls RecreatePath when finished.
    /// </summary>
    /// <param name="aMaterial">The terrain material! Usually from a terrain material prefab.</param>
    /// <param name="aForceUpdate">Force it to set the material, even if it's already the set material, or no?</param>
    public  void                        ForceMaterial   (Ferr2DT_TerrainMaterial aMaterial, bool aForceUpdate)
    {
        if (terrainMaterial != aMaterial || aForceUpdate)
        {
            terrainMaterial = aMaterial;

            // copy the materials into the renderer
            Material[] newMaterials = new Material[] {
                aMaterial.edgeMaterial,
                aMaterial.fillMaterial
            };
            renderer.sharedMaterials = newMaterials;

            // if we only have a top material, they probably don't want to split corners
            if (!terrainMaterial.Has(Ferr2DT_TerrainDirection.Left) &&
                !terrainMaterial.Has(Ferr2DT_TerrainDirection.Right))
            {
                splitCorners = false;
            }
            else
            {
                splitCorners = true;
            }

            RecreatePath();
        }
    }
	
    private Ferr2DT_SegmentDescription	GetDescription	(List<Vector2> aSegment) {
        Ferr2DT_TerrainDirection dir = Ferr2D_Path.GetDirection(aSegment, 0, fill == Ferr2DT_FillMode.InvertedClosed);
        return terrainMaterial.GetDescriptor(dir);
	}
    private List<List<Vector2>>         GetSegments     (List<Vector2> aPath)
    {
        List<List<Vector2>> segments = new List<List<Vector2>>();
        if (splitCorners)
        {
            segments = Ferr2D_Path.GetSegments(aPath);
        }
        else
        {
            segments.Add(aPath);
        }
        if (path.closed && smoothPath == false)
        {
            Ferr2D_Path.CloseEnds(ref segments, splitCorners);
        }
        return segments;
    }

    public  Vector2                     GetOffset       (List<Vector2> aSegment, int i, bool aClosed) {
        Ferr2DT_TerrainDirection prev = Ferr2D_Path.GetDirection(aSegment, i-1, fill == Ferr2DT_FillMode.InvertedClosed, aClosed);
        Ferr2DT_TerrainDirection next = Ferr2D_Path.GetDirection(aSegment, i,   fill == Ferr2DT_FillMode.InvertedClosed, aClosed);
        int prevID = (int)prev;
        int nextID = (int)next;

        Vector2 result = new Vector2(0,0);
        if (prev == Ferr2DT_TerrainDirection.Left || prev == Ferr2DT_TerrainDirection.Right)
        {
            result.x += surfaceOffset[prevID];
        }
        else if (next == Ferr2DT_TerrainDirection.Left || next == Ferr2DT_TerrainDirection.Right) {
            result.x += surfaceOffset[nextID];
        }

        if (prev == Ferr2DT_TerrainDirection.Top || prev == Ferr2DT_TerrainDirection.Bottom)
        {
            result.y += surfaceOffset[prevID];
        }
        else if (next == Ferr2DT_TerrainDirection.Top || next == Ferr2DT_TerrainDirection.Bottom) {
            result.y += surfaceOffset[nextID];
        }

        return result;
    }
	#endregion
}
